diff options
Diffstat (limited to 'app/[lng]')
| -rw-r--r-- | app/[lng]/auth/reset-password/page.tsx | 47 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/b-rfq/page.tsx | 3 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx | 18 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx | 65 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/evaluation/page.tsx | 19 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/layout.tsx | 11 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/password-policy/page.tsx | 63 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/document-list/layout.tsx | 5 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/settings/layout.tsx | 68 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/settings/page.tsx | 224 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/settings/preferences/page.tsx | 17 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/system/layout.tsx | 71 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/system/page.tsx | 62 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/system/permissions/page.tsx | 17 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/system/roles/page.tsx | 68 | ||||
| -rw-r--r-- | app/[lng]/privacy/page.tsx | 5 |
16 files changed, 673 insertions, 90 deletions
diff --git a/app/[lng]/auth/reset-password/page.tsx b/app/[lng]/auth/reset-password/page.tsx new file mode 100644 index 00000000..f49e5d86 --- /dev/null +++ b/app/[lng]/auth/reset-password/page.tsx @@ -0,0 +1,47 @@ +// app/[lng]/auth/reset-password/page.tsx + +import { redirect } from 'next/navigation'; +import { validateResetTokenAction } from '@/lib/users/auth/partners-auth'; +import InvalidTokenPage from '@/components/login/InvalidTokenPage'; +import ResetPasswordForm from '@/components/login/reset-password'; +import { getPasswordPolicy } from '@/lib/users/auth/passwordUtil'; + +interface Props { + searchParams: { token?: string }; +} + +export default async function ResetPasswordPage({ searchParams }: Props) { + const token = searchParams.token; + + // 토큰이 없는 경우 로그인 페이지로 리다이렉트 + if (!token) { + redirect('/partners'); + } + + // 서버에서 토큰 검증 + const tokenValidation = await validateResetTokenAction(token); + + // 토큰이 유효하지 않은 경우 + if (!tokenValidation.valid) { + return ( + <InvalidTokenPage + expired={tokenValidation.expired || false} + error={tokenValidation.error} + /> + ); + } + + // 패스워드 정책 로드 + const passwordPolicy = await getPasswordPolicy(); + + // 유효한 토큰인 경우 폼 표시 + return ( + <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> + <ResetPasswordForm + token={token} + userId={tokenValidation.userId!} + passwordPolicy={passwordPolicy} + /> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/b-rfq/page.tsx b/app/[lng]/evcp/(evcp)/b-rfq/page.tsx index 213e9127..a66d7b58 100644 --- a/app/[lng]/evcp/(evcp)/b-rfq/page.tsx +++ b/app/[lng]/evcp/(evcp)/b-rfq/page.tsx @@ -46,6 +46,8 @@ export default async function PQReviewPage(props: PQReviewPageProps) { }) ]) + console.log(search, "견적") + return ( <Shell className="gap-4"> <div className="flex items-center justify-between space-y-2"> @@ -60,7 +62,6 @@ export default async function PQReviewPage(props: PQReviewPageProps) { {/* Items처럼 직접 테이블 렌더링 */} <React.Suspense - key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 fallback={ <DataTableSkeleton columnCount={8} diff --git a/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx index 398005fa..a660c492 100644 --- a/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation-check-list/page.tsx @@ -32,6 +32,24 @@ async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) { return (
<Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 협력업체 평가기준표
+ </h2>
+ <p className="text-muted-foreground">
+ 협력업체 평가에 사용되는 평가기준표를 관리{" "}
+ {/* <span className="inline-flex items-center whitespace-nowrap">
+ <Ellipsis className="size-3" />
+ <span className="ml-1">버튼</span>
+ </span>
+ 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */}
+ </p>
+ </div>
+ </div>
+ </div>
+
<Suspense fallback={<Skeleton className="h-7 w-52" />}>
{/* <DateRangePicker
triggerSize="sm"
diff --git a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx index d60f695a..088ae75b 100644 --- a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx @@ -26,66 +26,7 @@ interface EvaluationTargetsPageProps { searchParams: Promise<SearchParams> } -// 프로세스 안내 팝오버 컴포넌트 -function ProcessGuidePopover() { - return ( - <Popover> - <PopoverTrigger asChild> - <Button variant="ghost" size="icon" className="h-6 w-6"> - <HelpCircle className="h-4 w-4 text-muted-foreground" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-96" align="start"> - <div className="space-y-3"> - <div className="space-y-1"> - <h4 className="font-medium">평가 대상 확정 프로세스</h4> - <p className="text-sm text-muted-foreground"> - 발주실적을 기반으로 평가 대상을 확정하는 절차입니다. - </p> - </div> - <div className="space-y-3 text-sm"> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 1 - </div> - <div> - <p className="font-medium">발주실적 기반 자동 추출</p> - <p className="text-muted-foreground">전년도 10월 ~ 해당년도 9월 발주실적에서 업체 목록을 자동으로 생성합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 2 - </div> - <div> - <p className="font-medium">담당자 지정</p> - <p className="text-muted-foreground">각 평가 대상별로 5개 부서(발주/조달/품질/설계/CS)의 담당자를 지정합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 3 - </div> - <div> - <p className="font-medium">검토 및 의견 수렴</p> - <p className="text-muted-foreground">모든 담당자가 평가 대상 적합성을 검토하고 의견을 제출합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 4 - </div> - <div> - <p className="font-medium">최종 확정</p> - <p className="text-muted-foreground">모든 담당자 의견이 일치하면 평가 대상으로 최종 확정됩니다.</p> - </div> - </div> - </div> - </div> - </PopoverContent> - </Popover> - ) -} + export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { const searchParams = await props.searchParams @@ -131,7 +72,7 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage <Badge variant="outline" className="text-sm"> {currentEvaluationYear}년도 </Badge> - <ProcessGuidePopover /> + </div> </div> </div> @@ -162,10 +103,12 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage /> } > + {currentEvaluationYear && <EvaluationTargetsTable promises={promises} evaluationYear={currentEvaluationYear} /> +} </React.Suspense> </Shell> ) diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx index 3ae3272a..ead61077 100644 --- a/app/[lng]/evcp/(evcp)/evaluation/page.tsx +++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx @@ -17,6 +17,8 @@ import { import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" +import { getPeriodicEvaluations } from "@/lib/evaluation/service" +import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" export const metadata: Metadata = { title: "협력업체 정기평가", @@ -93,25 +95,11 @@ function getDefaultEvaluationYear() { return new Date().getFullYear() } -function searchParamsPeriodicEvaluationsCache() { - // TODO: 실제 파서 구현 - return { - parse: (params: any) => params - } -} -async function getPeriodicEvaluations(params: any) { - // TODO: 실제 API 호출 구현 - return { - data: [], - total: 0, - pageCount: 0 - } -} export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { const searchParams = await props.searchParams - const search = searchParamsPeriodicEvaluationsCache().parse(searchParams) + const search = searchParamsEvaluationsCache.parse(searchParams) const validFilters = getValidFilters(search.filters || []) // 기본 필터 처리 @@ -150,7 +138,6 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations <Badge variant="outline" className="text-sm"> {currentEvaluationYear}년도 </Badge> - <ProcessGuidePopover /> </div> </div> </div> diff --git a/app/[lng]/evcp/(evcp)/system/layout.tsx b/app/[lng]/evcp/(evcp)/system/layout.tsx index 62f3e845..7e8f69d0 100644 --- a/app/[lng]/evcp/(evcp)/system/layout.tsx +++ b/app/[lng]/evcp/(evcp)/system/layout.tsx @@ -28,7 +28,7 @@ export default async function SettingsLayout({ const sidebarNavItems = [ { - title: "SHI Users", + title: "삼성중공업 사용자", href: `/${lng}/evcp/system`, }, { @@ -36,13 +36,18 @@ export default async function SettingsLayout({ href: `/${lng}/evcp/system/roles`, }, { - title: "Permissions", + title: "권한 통제", href: `/${lng}/evcp/system/permissions`, }, { - title: "Vendor Users", + title: "협력업체 사용자", href: `/${lng}/evcp/system/admin-users`, }, + + { + title: "비밀번호 정책", + href: `/${lng}/evcp/system/password-policy`, + }, ] diff --git a/app/[lng]/evcp/(evcp)/system/password-policy/page.tsx b/app/[lng]/evcp/(evcp)/system/password-policy/page.tsx new file mode 100644 index 00000000..0f14fefe --- /dev/null +++ b/app/[lng]/evcp/(evcp)/system/password-policy/page.tsx @@ -0,0 +1,63 @@ +// app/admin/password-policy/page.tsx + +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { AlertTriangle } from "lucide-react" +import SecuritySettingsTable from "@/components/system/passwordPolicy" +import { getSecuritySettings } from "@/lib/password-policy/service" + + +export default async function PasswordPolicyPage() { + try { + // 보안 설정 데이터 로드 + const securitySettings = await getSecuritySettings() + + return ( + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={4} + searchableColumnCount={0} + filterableColumnCount={0} + cellWidths={["20rem", "30rem", "15rem", "10rem"]} + shrinkZero + /> + } + > + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> + <p className="text-sm text-muted-foreground"> + 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. + </p> + </div> + <Separator /> + <SecuritySettingsTable initialSettings={securitySettings} /> + </div> + </React.Suspense> + ) + } catch (error) { + console.error('Failed to load security settings:', error) + + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> + <p className="text-sm text-muted-foreground"> + 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. + </p> + </div> + <Separator /> + <Alert variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. + </AlertDescription> + </Alert> + </div> + ) + } +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/document-list/layout.tsx b/app/[lng]/partners/(partners)/document-list/layout.tsx index 0eb9d27b..8d486113 100644 --- a/app/[lng]/partners/(partners)/document-list/layout.tsx +++ b/app/[lng]/partners/(partners)/document-list/layout.tsx @@ -22,7 +22,8 @@ export default async function VendorDocuments({ // const vendorId = "17" const idAsNumber = Number(vendorId) - const projects = await getVendorProjectsAndContracts(idAsNumber) + const projects = await getVendorProjectsAndContracts(idAsNumber); + const filteredProjects = projects.filter(v=>v.projectType === "plant") // 레이아웃 설정 쿠키 가져오기 @@ -39,7 +40,7 @@ export default async function VendorDocuments({ return ( <Shell className="gap-2"> - <VendorDocumentListClient projects={projects}> + <VendorDocumentListClient projects={filteredProjects}> {children} </VendorDocumentListClient> </Shell> diff --git a/app/[lng]/partners/(partners)/settings/layout.tsx b/app/[lng]/partners/(partners)/settings/layout.tsx new file mode 100644 index 00000000..6f373567 --- /dev/null +++ b/app/[lng]/partners/(partners)/settings/layout.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "Settings", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Account", + href: `/${lng}/evcp/settings`, + }, + { + title: "Preferences", + href: `/${lng}/evcp/settings/preferences`, + } + + + ] + + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + <h2 className="text-2xl font-bold tracking-tight">Settings</h2> + <p className="text-muted-foreground"> + Manage your account settings and preferences. + </p> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/5"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1 ">{children}</div> + </div> + </div> + </section> + </div> + + + </> + ) +} diff --git a/app/[lng]/partners/(partners)/settings/page.tsx b/app/[lng]/partners/(partners)/settings/page.tsx new file mode 100644 index 00000000..d831f0f4 --- /dev/null +++ b/app/[lng]/partners/(partners)/settings/page.tsx @@ -0,0 +1,224 @@ +// app/settings/page.tsx (인증 방식 기반 개선된 버전) +"use client" + +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/components/settings/account-form" +import { SimpleReAuthModal } from "@/components/auth/simple-reauth-modal" +import { Skeleton } from "@/components/ui/skeleton" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { useSettingsAccess } from "@/hooks/use-settings-access" +import { Shield, User, Building, ArrowLeft, Mail, Key, Smartphone } from "lucide-react" +import { useRouter } from "next/navigation" +import React from "react" + +// 인증 방식별 아이콘과 라벨 +const authMethodConfig = { + otp: { icon: Smartphone, label: "OTP Authentication", color: "bg-blue-50 text-blue-700 border-blue-300" }, + email: { icon: Mail, label: "Email Authentication", color: "bg-gray-50 text-gray-700 border-gray-300" }, + sgips: { icon: Building, label: "S-Gips Enterprise", color: "bg-purple-50 text-purple-700 border-purple-300" }, + saml: { icon: Key, label: "SAML SSO", color: "bg-green-50 text-green-700 border-green-300" }, +} + +export default function SettingsAccountPage() { + const router = useRouter() + const { + accessType, + showReAuthModal, + isAuthenticated, + userEmail, + userId, + userDomain, + authMethod, + handleReAuthSuccess, + forceReAuth, + } = useSettingsAccess({ + validDuration: 5 * 60 * 1000, // 5분 + sgipsRedirectPath: "/partners/dashboard", + }) + + // 로딩 상태 + if (accessType === 'loading') { + return ( + <div className="space-y-6"> + <div> + <Skeleton className="h-7 w-24" /> + <Skeleton className="h-4 w-96 mt-2" /> + </div> + <Separator /> + <div className="space-y-4"> + <Skeleton className="h-10 w-full" /> + <Skeleton className="h-10 w-full" /> + <Skeleton className="h-10 w-full" /> + </div> + </div> + ) + } + + // 인증되지 않은 상태 + if (accessType === 'unauthenticated') { + return ( + <div className="text-center py-12"> + <div className="mx-auto h-12 w-12 rounded-full bg-red-100 flex items-center justify-center mb-4"> + <User className="h-6 w-6 text-red-600" /> + </div> + <h3 className="text-lg font-medium mb-2">Authentication Required</h3> + <p className="text-muted-foreground mb-4"> + Please sign in to access account settings. + </p> + <Button onClick={() => router.push("/auth/login")}> + Sign In + </Button> + </div> + ) + } + + // S-Gips 사용자 접근 차단 + if (accessType === 'blocked_sgips') { + return ( + <div className="text-center py-12"> + <div className="mx-auto h-12 w-12 rounded-full bg-purple-100 flex items-center justify-center mb-4"> + <Building className="h-6 w-6 text-purple-600" /> + </div> + <h3 className="text-lg font-medium mb-2">Enterprise Account</h3> + <p className="text-muted-foreground mb-2"> + Your account is managed through S-Gips enterprise system. + </p> + <p className="text-sm text-muted-foreground mb-4"> + Domain: <span className="font-medium">{userDomain}</span> + </p> + <Button + onClick={() => router.push("/dashboard")} + className="flex items-center gap-2" + > + <ArrowLeft className="h-4 w-4" /> + Back to Dashboard + </Button> + </div> + ) + } + + // 재인증 대기 상태 + if (accessType === 'reauth_required') { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Account Settings</h3> + <p className="text-sm text-muted-foreground"> + Update your account settings and manage your profile information. + </p> + </div> + <Separator /> + + <div className="text-center py-12"> + <div className="mx-auto h-16 w-16 rounded-full bg-amber-100 flex items-center justify-center mb-4"> + <Shield className="h-8 w-8 text-amber-600 animate-pulse" /> + </div> + <h3 className="text-lg font-medium mb-2">Security Verification Required</h3> + <p className="text-muted-foreground mb-4"> + Please verify your password to access account settings. + </p> + + {/* 인증 방식 표시 */} + {authMethod && authMethodConfig[authMethod] && ( + <div className="flex items-center justify-center gap-2 mb-4"> + {React.createElement(authMethodConfig[authMethod].icon, { + className: "h-4 w-4 text-muted-foreground" + })} + <Badge variant="outline" className={authMethodConfig[authMethod].color}> + {authMethodConfig[authMethod].label} + </Badge> + </div> + )} + </div> + + <SimpleReAuthModal + isOpen={showReAuthModal} + onSuccess={handleReAuthSuccess} + userEmail={userEmail} + /> + </div> + ) + } + + // 접근 허용 상태 + const currentAuthConfig = authMethod && authMethodConfig[authMethod] + + return ( + <div className="space-y-6"> + <div> + <div className="flex items-center justify-between"> + <div> + <h3 className="text-lg font-medium">Account Settings</h3> + <p className="text-sm text-muted-foreground"> + Update your account settings and manage your profile information. + </p> + </div> + + {/* 보안 상태 표시 */} + <div className="flex items-center gap-2"> + <div className="flex items-center gap-2 px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm"> + <Shield className="h-4 w-4" /> + <span>Verified</span> + </div> + </div> + </div> + + {/* 사용자 정보 및 인증 방식 표시 */} + <div className="mt-4 p-4 bg-slate-50 border border-slate-200 rounded-lg"> + <div className="flex items-center justify-between"> + <div className="space-y-2"> + <div className="flex items-center gap-3"> + <div className="flex items-center gap-2"> + <div className="h-2 w-2 bg-green-500 rounded-full"></div> + <span className="text-sm font-medium text-slate-900"> + {userEmail} + </span> + </div> + + {/* 인증 방식 뱃지 */} + {currentAuthConfig && ( + <div className="flex items-center gap-1"> + {React.createElement(currentAuthConfig.icon, { + className: "h-3 w-3" + })} + <Badge variant="outline" className={`text-xs ${currentAuthConfig.color}`}> + {currentAuthConfig.label} + {userDomain && authMethod === 'saml' && ` (${userDomain})`} + </Badge> + </div> + )} + </div> + + {/* 도메인 정보 */} + {userDomain && ( + <div className="text-xs text-slate-600"> + Domain: <span className="font-medium">{userDomain}</span> + </div> + )} + </div> + + <div className="flex items-center gap-2"> + {/* 이메일 인증 사용자만 재인증 버튼 표시 */} + {authMethod === 'email' && ( + <Button + variant="outline" + size="sm" + onClick={forceReAuth} + className="text-amber-700 border-amber-300 hover:bg-amber-50" + > + Re-verify + </Button> + )} + </div> + </div> + </div> + </div> + + <Separator /> + + {/* 메인 콘텐츠 */} + <AccountForm /> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/settings/preferences/page.tsx b/app/[lng]/partners/(partners)/settings/preferences/page.tsx new file mode 100644 index 00000000..e2a88021 --- /dev/null +++ b/app/[lng]/partners/(partners)/settings/preferences/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/components/settings/appearance-form" + +export default function SettingsAppearancePage() { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Preference</h3> + <p className="text-sm text-muted-foreground"> + Customize the preference of the app. + </p> + </div> + <Separator /> + <AppearanceForm /> + </div> + ) +} diff --git a/app/[lng]/partners/(partners)/system/layout.tsx b/app/[lng]/partners/(partners)/system/layout.tsx new file mode 100644 index 00000000..504570bb --- /dev/null +++ b/app/[lng]/partners/(partners)/system/layout.tsx @@ -0,0 +1,71 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "System Setting", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "사용자", + href: `/${lng}/evcp/system`, + }, + { + title: "Roles", + href: `/${lng}/evcp/system/roles`, + }, + { + title: "권한 통제", + href: `/${lng}/evcp/system/permissions`, + }, + + ] + + + return ( + <> + <div className="container py-6"> + <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> + <div className="hidden space-y-6 p-10 pb-16 md:block"> + <div className="space-y-0.5"> + <h2 className="text-2xl font-bold tracking-tight">시스템 설정</h2> + <p className="text-muted-foreground"> + 사용자, 롤, 접근 권한을 관리하세요. + </p> + </div> + <Separator className="my-6" /> + <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> + <aside className="-mx-4 lg:w-1/5"> + <SidebarNav items={sidebarNavItems} /> + </aside> + <div className="flex-1 ">{children}</div> + </div> + </div> + </section> + </div> + + + </> + ) +} diff --git a/app/[lng]/partners/(partners)/system/page.tsx b/app/[lng]/partners/(partners)/system/page.tsx index a1e9f8be..1224851b 100644 --- a/app/[lng]/partners/(partners)/system/page.tsx +++ b/app/[lng]/partners/(partners)/system/page.tsx @@ -1,8 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import * as React from "react" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/admin-users/validations" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" +import { getAllRolesbyVendor, getUserCountGroupByRoleAndVendor, getVendorUsers } from "@/lib/vendor-users/service" +import { VendorUserTable } from "@/lib/vendor-users/table/ausers-table" -export default function Pages() { - return ( - <> - test - </> - ) - }
\ No newline at end of file +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function SystemUserPage(props: IndexPageProps) { + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorUsers({ + ...search, + filters: validFilters, + }), + getUserCountGroupByRoleAndVendor(), + getAllRolesbyVendor() + ]) + + return ( + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "12rem", "12rem", "12rem"]} + shrinkZero + /> + } + > + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Users</h3> + <p className="text-sm text-muted-foreground"> + 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. + </p> + </div> + <Separator /> + <VendorUserTable promises={promises} /> + </div> + </React.Suspense> + + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/system/permissions/page.tsx b/app/[lng]/partners/(partners)/system/permissions/page.tsx new file mode 100644 index 00000000..fe33f920 --- /dev/null +++ b/app/[lng]/partners/(partners)/system/permissions/page.tsx @@ -0,0 +1,17 @@ +import PermissionsTreeVendor from "@/components/system/permissionsTreeVendor" +import { Separator } from "@/components/ui/separator" + +export default function PermissionsPage() { + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Permissions</h3> + <p className="text-sm text-muted-foreground"> + Set permissions to the menu by Role + </p> + </div> + <Separator /> + <PermissionsTreeVendor/> + </div> + ) +} diff --git a/app/[lng]/partners/(partners)/system/roles/page.tsx b/app/[lng]/partners/(partners)/system/roles/page.tsx new file mode 100644 index 00000000..fe074600 --- /dev/null +++ b/app/[lng]/partners/(partners)/system/roles/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/roles/validations" +import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" +import { RolesTable } from "@/lib/roles/table/roles-table" +import { getRolesWithCount } from "@/lib/roles/services" +import { getUsersAll } from "@/lib/users/service" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const search2 = searchParamsCache2.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRolesWithCount({ + ...search, + filters: validFilters, + }), + + + ]) + + + const promises2 = Promise.all([ + getUsersAll({ + ...search2, + filters: validFilters, + }, "evcp"), + ]) + + + return ( + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium">Role Management</h3> + <p className="text-sm text-muted-foreground"> + 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. + </p> + </div> + <Separator /> + <RolesTable promises={promises} promises2={promises2} /> + </div> + </React.Suspense> + + ) +} diff --git a/app/[lng]/privacy/page.tsx b/app/[lng]/privacy/page.tsx new file mode 100644 index 00000000..28881db2 --- /dev/null +++ b/app/[lng]/privacy/page.tsx @@ -0,0 +1,5 @@ +import { PrivacyPolicyPage } from "@/components/login/privacy-policy-page" + +export default function Privacy() { + return <PrivacyPolicyPage /> +}
\ No newline at end of file |
